Tasks are not immune to exceptions. An unhandled
exception raised in a task is handled differently by the common language
runtime (CLR) than a typical exception. Unhandled exceptions in tasks
are not handled in the context of the task, but in the context of the
joining thread. Essentially, unhandled exceptions in a task are deferred
and propagated to the joining thread. The joining thread can then
observe the exception as the observer.
The unhandled exception propagated to the observer is wrapped in an AggregateException object. If you’re waiting on multiple tasks, the AggregateException might aggregate more than one unhandled exception from different parallel tasks. If a single exception is raised, the InnerException property of the AggregateException contains the original exception. For multiple exceptions, the InnerExceptions property returns a collection of Exception objects—one for each unhandled exception.
There are many ways to observe an unhandled exception raised in a task. Waiting for the task will observe the exception: Wait, WaitAll, or WaitAny methods. The joining thread then becomes the observer and is responsible for handling the exception. If the Wait method is within the scope of a try block (protected code), the otherwise unhandled exception of a task can be caught. You can then unbundle the AggregateException object and examine the original exception(s) in the InnerException or InnerExceptions property.
Remember that Parallel.Invoke ends with an implicit Task.WaitAll. This blocks the joining thread until all the tasks have completed. When Parallel.Invoke returns, any unhandled exceptions are observed, and if the code is running within a try block, you can handle the exception(s) in a catch block.
After scheduling a task, the TPL tracks the status of that task. The status is exposed as the Task.Status property, which is a TaskStatus type. TaskStatus is an enumeration. Some of the possible values are TaskStatus.WaitingToRun, TaskStatus.Running, TaskStatus.RanToCompletion, and TaskStatus.Canceled, which are self-explanatory. When a task raises an unhandled exception, the task status is TaskStatus.Faulted.
The following image shows a sequence diagram of a task that throws an exception. The exception is observed in a try block and caught in the subsequent catch block. Thicker vertical lines represent tasks executing on a thread.
Here are the steps in the sequence diagram.
Within a try block, invoke TaskA and TaskB. TaskB has the longer duration.
TaskA throws a divide-by-zero exception, which is wrapped in an AggregateException.
When TaskB completes, the WaitAll observes the unhandled exception raised by TaskA.
The catch block retrieves the original exception as the InnerException.
Note
that unhandled exceptions must be observed before the related task is
garbage-collected; otherwise, the unhandled exception might crash the
application. Here’s an example of successfully handling an unhandled
exception in a parallel task.
Create a task that throws an unhandled exception that is observed with a Task.Wait method
Create a console application. In the Main function, create a task reference, which is set to null.
Task TaskA = null;
Start a try/catch block. You will enter code guarded for an exception in the try block.
try {
In the try
block, you will create and start a new task. Initialize the task with a
lambda expression. In the lambda expression, define two integer
variables. Set one of the integers to zero. Divide by the integer
variable that has the zero value to raise the divide-by-zero exception.
TaskA = Task.Factory.StartNew(() => {
int a = 5, b = 0;
a /= b;});
Wait for the task to complete.
TaskA.Wait(); }
In the catch statement, catch the AggregateException exception. This will catch and observe the unhandled exception from the task.
catch(AggregateException ae) {
In the catch block, display the task status and inner exception, which contains the original exception.
Build and run the application.
Here is the complete application.
class Program
{
static void Main(string[] args)
{
Task TaskA = null;
try {
TaskA = Task.Factory.StartNew(() => {
int a = 5, b = 0;
a /= b;
});
TaskA.Wait();
}
catch(AggregateException ae) {
Console.WriteLine("Task has "+TaskA.Status.ToString());
Console.WriteLine(ae.InnerException);
}
}
}
Observe and iterate unhandled exceptions from three separate tasks
Different from the previous example, this walkthrough lists the separate exceptions thrown from different tasks.
Create a console application. Before the Main function, define MethodA, MethodB, and MethodC methods. Each method throws an explicit exception. In the exception constructor, provide the name of the task.
static void MethodA() { throw new Exception("TaskA Exception"); }
static void MethodB() { throw new Exception("TaskB Exception"); }
static void MethodC() { throw new Exception("TaskC Exception"); }
Start a try/catch block. You will enter code guarded for an exception in the try block.
try {
In the try block, create and start three tasks. Each task is initialized with a different method.
var TaskA=Task.Factory.StartNew(MethodA);
var TaskB=Task.Factory.StartNew(MethodB);
var TaskC=Task.Factory.StartNew(MethodC);
Use the Task.WaitAll method so that you can wait for the tasks and observe any exception.
Task.WaitAll(new Task[] {TaskA, TaskB, TaskC}); }
In the catch statement, catch the AggregateException exception. This will catch all unhandled exceptions of the tasks.
catch (AggregateException ae) {
In the catch block, iterate the AggregateException.InnerExceptions property and display each individual exception.
Build and run the application.
Here is the complete application.
class Program
{
static void MethodA() { throw new Exception("TaskA Exception"); }
static void MethodB() { throw new Exception("TaskB Exception"); }
static void MethodC() { throw new Exception("TaskC Exception"); }
static void Main(string[] args)
{
try {
var TaskA=Task.Factory.StartNew(MethodA);
var TaskB=Task.Factory.StartNew(MethodB);
var TaskC=Task.Factory.StartNew(MethodC);
Task.WaitAll(new Task[] {TaskA, TaskB, TaskC});
}
catch (AggregateException ae) {
foreach (var ex in ae.InnerExceptions) {
Console.WriteLine(ex.Message);
}
}
}
}
The following code is similar to the previous example, except that it calls Parallel.Invoke instead of TaskFactory.StartNew.
try {
Parallel.Invoke(new Action[] { MethodA, MethodB, MethodC });
}
catch (AggregateException ae){
foreach (var ex in ae.InnerExceptions) {
Console.WriteLine(ex.Message);
}
}
The previous examples use a foreach loop to inspect and handle unhandled exceptions from different tasks. Alternatively, you can call the AggregateException.Handle
method, which takes a callback function (delegate) as its only
parameter. The delegate accepts the original exception as the single
parameter and returns a Boolean: Task<TResult>. The return value indicates whether the unhandled exception was handled. Return true when you successfully handle the exception, otherwise return false. If you return false, the exception will continue to propagate up the call stack as a new aggregate exception. This new AggregateException will contain all the exceptions where the handler delegate returned false.
AggregateException
calls the provided callback method for the exception from a task. For
example, if there are four unhandled exceptions, it calls the callback
four times. You’ll practice handling exceptions with a callback in the
next exercise.
Handle task exceptions with a callback function
Create a console application. Before the Main function, define MethodA, MethodB, and MethodC methods. Each method throws an explicit exception. In the exception constructor, provide the name of the task.
static void MethodA() { throw new Exception("TaskA Exception"); }
static void MethodB() { throw new Exception("TaskB Exception"); }
static void MethodC() { throw new Exception("TaskC Exception"); }
Start a try/catch block for handling exceptions.
try {
In the try block, execute the three methods as tasks by using the Parallel.Invoke method.
Parallel.Invoke(new Action[] { MethodA, MethodB, MethodC }); }
In the catch statement, catch the AggregateException exception. This will catch all unhandled exceptions of the tasks.
catch (AggregateException ae) {
Call the Handle method on the AggregateException object. Enter the callback as a lambda expression.
ae.Handle(ex => {
In the lambda expression, display the current exception message. Return true to indicate that the exception was handled.
Build and run the application.
Here is the complete application.
class Program
{
static void MethodA() { throw new Exception("TaskA Exception"); }
static void MethodB() { throw new Exception("TaskB Exception"); }
static void MethodC() { throw new Exception("TaskC Exception"); }
static void Main(string[] args)
{
try
{
Parallel.Invoke(new Action[] { MethodA, MethodB, MethodC });
}
catch (AggregateException ae)
{
ae.Handle(ex =>
{
Console.WriteLine(ex.Message);
return true;
});
}
}
}
As mentioned previously, a task can return a value, which you can inspect by using the Task<TResult>.Result property. If the Task has not completed, the current thread is suspended until the task finishes and returns a value. In addition, the Task<TResult>.Result property will observe an unhandled exception of the task, if any. Here is an example.
try{
var TaskA=Task<int>.Factory.StartNew(() =>{
throw new DivideByZeroException();
return 42;
});
// Unhandled exception observed.
Console.WriteLine(TaskA.Result);
}
catch (AggregateException ae) {
// handle exception
Console.WriteLine(ae.InnerException.Message);
}